import java.util.Collection;
import java.util.Queue;
import java.util.LinkedList;
import java.util.Date;
import java.util.Map;
import java.util.HashMap;
import java.util.Random;

import org.json.JSONObject;
import org.json.JSONArray;
import org.json.JSONException;

public class Client
{

/* Code for the global set of Clients */
    private static Map<String,Client> map = new HashMap<String, Client>();

    /**
     * Finds whether a client with the given id exists.
     *
     * Named for consistency with <code>Map</code>.
     *
     * @param s  The id to check.
     */
    public static boolean containsKey(String s)
    {
        return map.containsKey(s);
    }

    /**
     * Retrieve a client with the given id.
     *
     * @param s  The id.
     */
    public static Client get(String s)
    {
        return map.get(s);
    }

    /**
     * Create a new client and add it to the global <code>Map</code>.
     *
     * Will assign a new random string id to the client.
     */
    public static Client addnew ()
    {
       String id; 
       Random r = new Random();
       do
       {
         id = Long.toString(Math.abs(r.nextLong()), 16);
       }
       while (id.length() < 5 && map.containsKey(id));

       map.put(id, new Client(id));
       return map.get(id);
    }

    /**
     * Delete a client from the map.
     *
     * Should be called when a client times out or deliberately unsubscribes.
     *
     * @param id  The id of the client to remove.
     */
    public static Client remove(String id)
    {
        return map.remove(id);
    }

    /**
     * Apply an action to every client in turn.
     *
     * This method is safe, i.e. Clients can be removed during the iteration.
     * Clients that are removed during the iteration will still be iterated 
     * over.
     *
     * @param a  The action to perform
     */
    public static void forEachClient (ClientAction a)
    {
        //Create a copy of the current status - so that
        //if the action removes the Client from the Map
        //all will still be well.
        Collection<Client> clients = new LinkedList<Client>();

        for (Map.Entry<String,Client> ent : map.entrySet())
        {
            clients.add(ent.getValue());
        }

        for (Client c : clients)
        {
            a.action(c);
        }
    }

/* Code for individual clients */

    public final String id;
    public Document document = null;
    private String alias;

    private Date touched;
    private Queue<JSONObject> sendQ = new LinkedList<JSONObject>();

    /**
     * Creates a new client with the given id.
     *
     * This id is used both by the network layer and the diff layer.
     *
     * @param id  The unique id to use.
     */
    public Client(String id)
    {
        this.id = id;
        this.touched = new Date();
    }

    /**
     * Sends a message to the client.
     *
     * In this case, as we are over HTTP, we just store the message until the 
     * client next polls the server.
     *
     * @param msg  The message to send.
     */
    public void send(JSONObject msg)
    {
        this.sendQ.add(msg);
    }

    /**
     * Formats the actual string we need to send to the browser.
     * 
     * As the client sends and recieves JSONObjects, but we may have multiple
     * things to say to them, send a JSONArray of JSONObjects.
     *
     * Also clears the queue of messages to be sent, so they are only sent once.
     *
     * @return  The string to be sent to the browser.
     */
    public String getResponse()
    {
        String resp = new JSONArray(sendQ).toString();
        sendQ = new LinkedList<JSONObject>();
        return resp;
    }

    /**
     * Constructs a representation of this user in JSON.
     *
     * @return  {"alias": this.alias, "id": this.id}
     */
    public JSONObject userInfo() throws JSONException
    {
        JSONObject user_info = new JSONObject();
        user_info.put("alias", this.alias);
        user_info.put("id", this.id);
        return user_info;
    }

    /**
     * Subscribes the user to the document under the given alias.
     *
     * Calls the document.subscribe() method with this client.
     *
     * @param alias
     * @param document
     */
    public void subscribe(String alias, Document document)
    {
        assert(this.document == null);
        this.document = document;
        this.alias = alias;
        document.subscribe(this);
    }

    /**
     * Updates the "last-seen" timestamp.
     */
    public void touch()
    {
        this.touched = new Date();
    }

    /**
     * Returns whether the client has been seen in the last ten seconds.
     *
     * XXX: this value shouldn't be hardcoded here!
     *
     * @return boolean
     */
    public boolean timedOut()
    {
        return new Date().after(new Date(this.touched.getTime() + 10000));
    }

    /**
     * Removes the client from the global client list, and from its document.
     *
     * Gets called when the client times out or deliberately unsubscribes,
     * calls the .unsubscribe() method on the document.
     */
    public void unsubscribe()
    {
        Client.remove(this.id);
        this.document.unsubscribe(this);
    }
}
